package org.apache.lucene.search;

/**
 * Copyright 2005 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

/** An alternative to BooleanScorer.
 * <br>Uses ConjunctionScorer, DisjunctionScorer, ReqOptScorer and ReqExclScorer.
 * <br>Implements skipTo(), and has no limitations on the numbers of added scorers.
 */
class BooleanScorer2 extends Scorer {
    private ArrayList requiredScorers = new ArrayList();
    private ArrayList optionalScorers = new ArrayList();
    private ArrayList prohibitedScorers = new ArrayList();

    private class Coordinator {
        int maxCoord = 0; // to be increased for each non prohibited scorer

        private float[] coordFactors = null;

        void init() { // use after all scorers have been added.
            coordFactors = new float[maxCoord + 1];
            Similarity sim = getSimilarity();
            for (int i = 0; i <= maxCoord; i++) {
                coordFactors[i] = sim.coord(i, maxCoord);
            }
        }

        int nrMatchers; // to be increased by score() of match counting scorers.

        void initDoc() {
            nrMatchers = 0;
        }

        float coordFactor() {
            return coordFactors[nrMatchers];
        }
    }

    private final Coordinator coordinator;

    /** The scorer to which all scoring will be delegated,
     * except for computing and using the coordination factor.
     */
    private Scorer countingSumScorer = null;

    /** The number of optionalScorers that need to match (if there are any) */
    private final int minNrShouldMatch;

    /** Create a BooleanScorer2.
     * @param similarity The similarity to be used.
     * @param minNrShouldMatch The minimum number of optional added scorers
     *                         that should match during the search.
     *                         In case no required scorers are added,
     *                         at least one of the optional scorers will have to
     *                         match during the search.
     */
    public BooleanScorer2(Similarity similarity, int minNrShouldMatch) {
        super(similarity);
        if (minNrShouldMatch < 0) {
            throw new IllegalArgumentException("Minimum number of optional scorers should not be negative");
        }
        coordinator = new Coordinator();
        this.minNrShouldMatch = minNrShouldMatch;
    }

    /** Create a BooleanScorer2.
     *  In no required scorers are added,
     *  at least one of the optional scorers will have to match during the search.
     * @param similarity The similarity to be used.
     */
    public BooleanScorer2(Similarity similarity) {
        this(similarity, 0);
    }

    public void add(final Scorer scorer, boolean required, boolean prohibited) {
        if (!prohibited) {
            coordinator.maxCoord++;
        }

        if (required) {
            if (prohibited) {
                throw new IllegalArgumentException("scorer cannot be required and prohibited");
            }
            requiredScorers.add(scorer);
        } else if (prohibited) {
            prohibitedScorers.add(scorer);
        } else {
            optionalScorers.add(scorer);
        }
    }

    /** Initialize the match counting scorer that sums all the
     * scores. <p>
     * When "counting" is used in a name it means counting the number
     * of matching scorers.<br>
     * When "sum" is used in a name it means score value summing
     * over the matching scorers
     */
    private void initCountingSumScorer() {
        coordinator.init();
        countingSumScorer = makeCountingSumScorer();
    }

    /** Count a scorer as a single match. */
    private class SingleMatchScorer extends Scorer {
        private Scorer scorer;
        private int lastScoredDoc = -1;

        SingleMatchScorer(Scorer scorer) {
            super(scorer.getSimilarity());
            this.scorer = scorer;
        }

        public float score() throws IOException {
            if (doc() > lastScoredDoc) {
                lastScoredDoc = doc();
                coordinator.nrMatchers++;
            }
            return scorer.score();
        }

        public int doc() {
            return scorer.doc();
        }

        public boolean next() throws IOException {
            return scorer.next();
        }

        public boolean skipTo(int docNr) throws IOException {
            return scorer.skipTo(docNr);
        }

        public Explanation explain(int docNr) throws IOException {
            return scorer.explain(docNr);
        }
    }

    private Scorer countingDisjunctionSumScorer(List scorers, int minMrShouldMatch)
    // each scorer from the list counted as a single matcher
    {
        return new DisjunctionSumScorer(scorers, minMrShouldMatch) {
            private int lastScoredDoc = -1;

            public float score() throws IOException {
                if (doc() > lastScoredDoc) {
                    lastScoredDoc = doc();
                    coordinator.nrMatchers += super.nrMatchers;
                }
                return super.score();
            }
        };
    }

    private static Similarity defaultSimilarity = new DefaultSimilarity();

    private Scorer countingConjunctionSumScorer(List requiredScorers) {
        // each scorer from the list counted as a single matcher
        final int requiredNrMatchers = requiredScorers.size();
        ConjunctionScorer cs = new ConjunctionScorer(defaultSimilarity) {
            private int lastScoredDoc = -1;

            public float score() throws IOException {
                if (doc() > lastScoredDoc) {
                    lastScoredDoc = doc();
                    coordinator.nrMatchers += requiredNrMatchers;
                }
                // All scorers match, so defaultSimilarity super.score() always has 1 as
                // the coordination factor.
                // Therefore the sum of the scores of the requiredScorers
                // is used as score.
                return super.score();
            }
        };
        Iterator rsi = requiredScorers.iterator();
        while (rsi.hasNext()) {
            cs.add((Scorer) rsi.next());
        }
        return cs;
    }

    private Scorer dualConjunctionSumScorer(Scorer req1, Scorer req2) { // non counting. 
        final int requiredNrMatchers = requiredScorers.size();
        ConjunctionScorer cs = new ConjunctionScorer(defaultSimilarity);
        // All scorers match, so defaultSimilarity super.score() always has 1 as
        // the coordination factor.
        // Therefore the sum of the scores of two scorers
        // is used as score.
        cs.add(req1);
        cs.add(req2);
        return cs;
    }

    /** Returns the scorer to be used for match counting and score summing.
     * Uses requiredScorers, optionalScorers and prohibitedScorers.
     */
    private Scorer makeCountingSumScorer() { // each scorer counted as a single matcher
        return (requiredScorers.size() == 0) ? makeCountingSumScorerNoReq() : makeCountingSumScorerSomeReq();
    }

    private Scorer makeCountingSumScorerNoReq() { // No required scorers
        if (optionalScorers.size() == 0) {
            return new NonMatchingScorer(); // no clauses or only prohibited clauses
        } else { // No required scorers. At least one optional scorer.
            // minNrShouldMatch optional scorers are required, but at least 1
            int nrOptRequired = (minNrShouldMatch < 1) ? 1 : minNrShouldMatch;
            if (optionalScorers.size() < nrOptRequired) {
                return new NonMatchingScorer(); // fewer optional clauses than minimum (at least 1) that should match
            } else { // optionalScorers.size() >= nrOptRequired, no required scorers
                Scorer requiredCountingSumScorer = (optionalScorers.size() > nrOptRequired) ? countingDisjunctionSumScorer(optionalScorers, nrOptRequired) : // optionalScorers.size() == nrOptRequired (all optional scorers are required), no required scorers
                                (optionalScorers.size() == 1) ? new SingleMatchScorer((Scorer) optionalScorers.get(0)) : countingConjunctionSumScorer(optionalScorers);
                return addProhibitedScorers(requiredCountingSumScorer);
            }
        }
    }

    private Scorer makeCountingSumScorerSomeReq() { // At least one required scorer.
        if (optionalScorers.size() < minNrShouldMatch) {
            return new NonMatchingScorer(); // fewer optional clauses than minimum that should match
        } else if (optionalScorers.size() == minNrShouldMatch) { // all optional scorers also required.
            ArrayList allReq = new ArrayList(requiredScorers);
            allReq.addAll(optionalScorers);
            return addProhibitedScorers(countingConjunctionSumScorer(allReq));
        } else { // optionalScorers.size() > minNrShouldMatch, and at least one required scorer
            Scorer requiredCountingSumScorer = (requiredScorers.size() == 1) ? new SingleMatchScorer((Scorer) requiredScorers.get(0)) : countingConjunctionSumScorer(requiredScorers);
            if (minNrShouldMatch > 0) { // use a required disjunction scorer over the optional scorers
                return addProhibitedScorers(dualConjunctionSumScorer( // non counting
                                requiredCountingSumScorer, countingDisjunctionSumScorer(optionalScorers, minNrShouldMatch)));
            } else { // minNrShouldMatch == 0
                return new ReqOptSumScorer(addProhibitedScorers(requiredCountingSumScorer), ((optionalScorers.size() == 1) ? new SingleMatchScorer((Scorer) optionalScorers.get(0)) : countingDisjunctionSumScorer(optionalScorers, 1))); // require 1 in combined, optional scorer.
            }
        }
    }

    /** Returns the scorer to be used for match counting and score summing.
     * Uses the given required scorer and the prohibitedScorers.
     * @param requiredCountingSumScorer A required scorer already built.
     */
    private Scorer addProhibitedScorers(Scorer requiredCountingSumScorer) {
        return (prohibitedScorers.size() == 0) ? requiredCountingSumScorer // no prohibited
                        : new ReqExclScorer(requiredCountingSumScorer, ((prohibitedScorers.size() == 1) ? (Scorer) prohibitedScorers.get(0) : new DisjunctionSumScorer(prohibitedScorers)));
    }

    /** Scores and collects all matching documents.
     * @param hc The collector to which all matching documents are passed through
     * {@link HitCollector#collect(int, float)}.
     * <br>When this method is used the {@link #explain(int)} method should not be used.
     */
    public void score(HitCollector hc) throws IOException {
        if (countingSumScorer == null) {
            initCountingSumScorer();
        }
        while (countingSumScorer.next()) {
            hc.collect(countingSumScorer.doc(), score());
        }
    }

    /** Expert: Collects matching documents in a range.
     * <br>Note that {@link #next()} must be called once before this method is
     * called for the first time.
     * @param hc The collector to which all matching documents are passed through
     * {@link HitCollector#collect(int, float)}.
     * @param max Do not score documents past this.
     * @return true if more matching documents may remain.
     */
    protected boolean score(HitCollector hc, int max) throws IOException {
        // null pointer exception when next() was not called before:
        int docNr = countingSumScorer.doc();
        while (docNr < max) {
            hc.collect(docNr, score());
            if (!countingSumScorer.next()) {
                return false;
            }
            docNr = countingSumScorer.doc();
        }
        return true;
    }

    public int doc() {
        return countingSumScorer.doc();
    }

    public boolean next() throws IOException {
        if (countingSumScorer == null) {
            initCountingSumScorer();
        }
        return countingSumScorer.next();
    }

    public float score() throws IOException {
        coordinator.initDoc();
        float sum = countingSumScorer.score();
        return sum * coordinator.coordFactor();
    }

    /** Skips to the first match beyond the current whose document number is
     * greater than or equal to a given target.
     * 
     * <p>When this method is used the {@link #explain(int)} method should not be used.
     * 
     * @param target The target document number.
     * @return true iff there is such a match.
     */
    public boolean skipTo(int target) throws IOException {
        if (countingSumScorer == null) {
            initCountingSumScorer();
        }
        return countingSumScorer.skipTo(target);
    }

    /** Throws an UnsupportedOperationException.
     * TODO: Implement an explanation of the coordination factor.
     * @param doc The document number for the explanation.
     * @throws UnsupportedOperationException
     */
    public Explanation explain(int doc) {
        throw new UnsupportedOperationException();
        /* How to explain the coordination factor?
           initCountingSumScorer();
           return countingSumScorer.explain(doc); // misses coord factor. 
         */
    }
}
